﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SharpHomeEngine;
using System.Threading;
using System.IO.Ports;
using Insteon.Messages;

namespace Insteon
{
    internal delegate void InsteonMessageDelegate(Messages.Message Message);



    public class InsteonInterface : DeviceInterface
    {
        Thread WorkerThread;
        Thread PortEventThread;
        SerialPort Port;
        const String PortLock = "PortLock";
        InsteonAddress ModemAddress = new InsteonAddress("AA.AA.AA");
        

        private Queue<Commands.Command> CommandQueue = new Queue<Commands.Command>();
        private List<Commands.Command> CommandsNotRespondedTo = new List<Commands.Command>(); //command is sent but awaiting response
        private Dictionary<InsteonAddress, InsteonDevice> InsteonDeviceDictionary = new Dictionary<InsteonAddress, InsteonDevice>();

        Messages.Insteon.StandardMessage LastStandardMessage = null;

        public InsteonInterface(StateEngine Engine, String Name, InsteonAddress ModemAddress, String CommPort)
            : base(Engine, Name)
        {
            Port = new SerialPort(CommPort);
            this.ModemAddress = ModemAddress;
        }

        #region perform requests

        internal void UnsolicitedTagChange(InsteonDevice Device, Tag Tag, object Value)
        {
            this.UnsolicitedTagStateChange(Device, Tag, Value);
        }

        public override void BeginRequest(Request4DeviceInterface Request)
        {
            if (Request is Request4TagStateChange)
            {
                BeginTagStateChange((Request4TagStateChange)Request);
            }
            if (Request is Request4TagState)
            {
                BeginTagStateReqest((Request4TagState)Request);
            }
            if (Request is Requests.InsteonRequest)
            {
                //insteon requests are smart enough to handle themselves
                Requests.InsteonRequest Req = (Requests.InsteonRequest)Request;
                Req.BeginExecute();
            }
        }

        /// <summary>
        /// the request4tagstatechange is not an insteon request and so the request itsself is not smart enought to handle its self.
        /// we must handle it explicitly here.
        /// </summary>
        /// <param name="Request"></param>
        private void BeginTagStateChange(Request4TagStateChange Request)
        {
            InsteonDevice InsteonDevice = GetDevice(Request);
            Commands.Command Command = InsteonDevice.GenerateStateChangeCommand(Request.Tag, Request.Value);
            if (Command == null)
            {
                Console.WriteLine("!!!!!!!!!!!! Command not created !!!!!!!!!!!!!!");
                return;
            }
            Command.Request = Request;
            this.QueueCommand(Command);
        }
        /// <summary>
        /// the request4tagstate is not an insteon request and so the request itsself is not smart enought to handle its self.
        /// we must handle it explicitly here.
        /// </summary>
        /// <param name="Request"></param>
        private void BeginTagStateReqest(Request4TagState Request)
        {
            InsteonDevice InsteonDevice = GetDevice(Request);
            Commands.Command Command = InsteonDevice.GenerateStateRequest(Request.Tag);
            Command.Request = Request;
            this.QueueCommand(Command);
        }

        #endregion

        #region startup and whatnot
        public override void Init()
        {

            base.Init();
        }

        public override void Start()
        {
            Port.BaudRate = 19200;
            Port.DataBits = 8;
            Port.StopBits = StopBits.One;
            Port.Parity = Parity.None;
            Port.Handshake = Handshake.None;
            Port.ReadBufferSize = 1000;
            Port.RtsEnable = true;
            Port.DtrEnable = true;
            Port.Open();

            WorkerThread = new Thread(new ThreadStart(Worker));
            WorkerThread.Start();
            PortEventThread = new Thread(new ThreadStart(PortReadThread));
            PortEventThread.Start();
        }
        #endregion

        #region command queue & processor

        internal void QueueCommand(Commands.Command Command)
        {
            CommandQueue.Enqueue(Command);
        }

        /// <summary>
        /// I process commands in the command queue;
        /// </summary>
        private void Worker()
        {
            while (true)
            {
                int Count = 0;
                lock (CommandQueue)
                {
                    Count = CommandQueue.Count;
                }
                if (Count <= 0)
                {
                    Thread.Sleep(1);
                    continue;
                }
                Commands.Command Command = null;
                lock (CommandQueue)
                {
                    Command = CommandQueue.Dequeue();
                }
                if (Command != null)
                {
                    lock (CommandsNotRespondedTo)
                    {
                        CommandsNotRespondedTo.Add(Command);
                    }

                    Command.SentTime = DateTime.Now;
                    Port.Write(Command.Buffer, 0, Command.Buffer.Length);
                    Console.WriteLine(Command);
                    Command.Wait(); //block until the communications is done or I timeout.
                }
            }
        }

        #endregion

        #region message reader

        const UInt16 MAX_BUFFER_SIZE = 1000;
        byte[] Buffer = new byte[2000];

        void PortReadThread()
        {
            byte[] Buffer = new byte[350];
            UInt16 BufferLength = 0;
            byte buff;
            UInt16 MessageSize = 0;
            while (true)
            {
                try
                {
                    //TODO: More robust command finding logic incase I lose my way. 
                    buff = (byte)Port.ReadByte(); //I block need to use this for MONO compatability.
                    
                    Console.Write("#{0:x2} ", buff);
                    
                    Buffer[BufferLength] = buff;
                    BufferLength++;
                    if ((BufferLength > 0) && (Buffer[0] != 0x02))
                    {
                        Console.WriteLine("We have a problem!");
                        //find the next 0x02 byte
                        for (UInt16 t = 0; t < BufferLength; t++)
                        {
                            if (Buffer[t] == 0x02){
                                //found one purge the buffer then gtfo
                                BufferLength = PurgeBuffer(Buffer, BufferLength, t);
                                break;
                            }
                        }
                        //if we didnt find another 0x02 byte lets loop back and wait for one.
                        continue;
                    }

                    //TODO: if the buffer is over a certain size and we havent found a message yet.
                    //handle that.

                    Messages.Message Recv = MessageFactory.Create(Buffer, BufferLength, out MessageSize);
                    
                    if (Recv != null)
                    {
                        Console.WriteLine("");
                        
                        //TODO: Check for duplicate messages, yes they happen.
                        //TODO: Drop the duplicate messages.

                        BufferLength = PurgeBuffer(Buffer, BufferLength, MessageSize);
                        HandleMessage(Recv);
                        
                    }
                }
                catch (Exception e)
                {
                    while (e != null)
                    {
                        Console.WriteLine(e.Message);
                        e = e.InnerException;
                    }
                }
            }
        }

        private static UInt16 PurgeBuffer(byte[] Buffer, UInt16 BufferLength, UInt16 PurgeLength)
        {
            for (int t = 0; t < BufferLength; t++)
            {
                    Buffer[t] = Buffer[t + PurgeLength];
            }

            BufferLength = (UInt16)((int)BufferLength - (int)PurgeLength);
            return BufferLength;
        }
        #endregion

        #region message handling

        private void HandleMessage(Messages.Message Message)
        {
            Commands.Command FoundCommand = null;
            List<Commands.Command> TimedOut = new List<Commands.Command>();

            //TODO: If the message is a standardmessage alllinkcleanup message then send an ack.
            //or does the modem do that?
            if (Message is Messages.Insteon.StandardMessage)
            {
                Messages.Insteon.StandardMessage Mess = (Messages.Insteon.StandardMessage)Message;
                if (LastStandardMessage != null)
                {
                    if (LastStandardMessage.From == Mess.From && LastStandardMessage.Command1 == Mess.Command1 && LastStandardMessage.Command2 == Mess.Command2)
                    {
                        if (Mess.ReceivedTime.Subtract(LastStandardMessage.ReceivedTime).Milliseconds < 500)
                        {
                            LastStandardMessage = Mess;
                            //Console.WriteLine(" Dupe Drop");
                            return; //disregard this repeat
                        }
                    }
                }
                LastStandardMessage = Mess;
            }
            

            lock (CommandsNotRespondedTo)
            {
                
                //take the message and loop thru all commands awaiting a response
                foreach (Commands.Command Command in CommandsNotRespondedTo)
                {
                    //a command should know if the message is a response to it.
                    //the command should return true if the message is a response to it.
                    //true or false have nothing to do if the command was successful.
                    //true or false only means the message is a response to the command
                    if (Command.IsResponse(Message))
                    {
                        Message.Matched = true;
                        FoundCommand = Command;
                    }

                    //figure timeout... cant keep uncompleted commands around forever.
                    if (Command.SentTime <= DateTime.Now.AddSeconds(-30))
                    {
                        Command.Error(); //complete the command
                        TimedOut.Add(Command); //we cannot remove commands here since we are in a foreach
                    }
                }
            }
            //we dont want to do too much inside a lock statement, like raise events.
            //so get out of the lock block and then handle the the rest

            //remove timed out commands here
            //since we cant to it in the above foreach loop.
            lock (CommandsNotRespondedTo)
            {
                foreach (Commands.Command Command in TimedOut)
                {
                    CommandsNotRespondedTo.Remove(Command);
                    //TODO: Do something
                    Console.WriteLine("{0}  {1} Command Timed Out", Command.SentTime.ToString("Hmmss.fff"), Command.Number);
                }
            }

            //if the message completed a command
            //then handle the completed command
            if (FoundCommand != null)
            {
                Console.WriteLine("{0} Matched", Message);
                if (FoundCommand.isComplete == true)
                {
                    CompletedCommandHandler(FoundCommand);
                }
            }
            
            //if the message is unmatched then its considered unsolicited.
            if (Message.Matched == false){
                HandleUnsolicitedMessage(Message);
            }
        }


        internal void CompletedCommandHandler(Commands.Command Command)
        {
            //how we handle the compeleted command depends on what initiated it.
            if (Command.Request != null)
            {
                if (Command.Request is Request4TagStateChange)
                {
                    CompleteRequest4TagStateChange(Command);
                }
                if (Command.Request is Request4TagState)
                {
                    CompleteRequest4TagState(Command);
                }
            }
            if (Command.InsteonRequest != null)
            {
                Command.InsteonRequest.CommandCompleted(Command);
            }
            lock (CommandsNotRespondedTo)
            {
                CommandsNotRespondedTo.Remove(Command);
            }
        }

        private static void CompleteRequest4TagStateChange(Commands.Command Command)
        {
            Command.Request.Complete(true, (bool)Command.isComplete);
            TimeSpan Duration = DateTime.Now.Subtract(Command.SentTime).Duration();
            //Console.WriteLine("Command {0} {1}  Completed Sucess={2}  Duration {3:0.000}s ", Command.SentTime.ToString("HHmmss.fff"), Command.Number, Command.isSuccessful, (Duration.TotalMilliseconds / 1000));

        }

        private static void CompleteRequest4TagState(Commands.Command Command)
        {
            Request4TagState Request = (Request4TagState)Command.Request;
            InsteonDevice Device = (InsteonDevice)Request.Device;
            Device.ReadStateRequestResponse(Command, Request.Tag);
            Request.Complete(Command.Value, true, (bool)Command.isComplete);
            TimeSpan Duration = DateTime.Now.Subtract(Command.SentTime).Duration();
            //Console.WriteLine("Command {0} {1}  Completed Sucess={2}  Duration {3:0.000}s ", Command.SentTime.ToString("HHmmss.fff"), Command.Number, Command.isSuccessful, (Duration.TotalMilliseconds / 1000));
        }

        /// <summary>
        /// messages received here were not a response from a command
        /// </summary>
        /// <param name="Message"></param>
        internal void HandleUnsolicitedMessage(Messages.Message Message){
            Console.WriteLine("{0} Unsolicited", Message);
            //the message may or may not need routed to a device.
            Message.RouteToDevice(this);
        }

        //called by messages needing routed.
        internal void RouteToTarget(Messages.Message Message, InsteonAddress Target)
        {
            if (Target != null)
            {
                if (InsteonDeviceDictionary.ContainsKey(Target))
                {
                    InsteonDevice Device = InsteonDeviceDictionary[Target];
                    Device.HandleUnsolicitedMessage(Message);
                }
            }
        }
        #endregion

        #region misc
        private void PrintBytes(byte[] bytes, int length)
        {
            StringBuilder sb = new StringBuilder(length * 2);
            for(int t = 0; t < length; t++)
            {
                sb.AppendFormat("{0:x2} ", bytes[t]);
            }
            Console.WriteLine(sb.ToString());
        }

        protected override Type DeviceType
        {
            get { return typeof(InsteonDevice); }
        }

        private static InsteonDevice GetDevice(Request4Tag Request)
        {
            if (!Request.Device.GetType().IsSubclassOf(typeof(InsteonDevice)))
            {
                throw new Exception("Device is not an insteon device");
            }
            InsteonDevice InsteonDevice = (InsteonDevice)Request.Device;
            return InsteonDevice;
        }

        public override void RegisterDevice(Device Device)
        {
            if (!Device.GetType().IsSubclassOf(typeof(InsteonDevice)))
            {
                throw new Exception("Device is not an insteon device");
            }
            InsteonDevice Dev = (InsteonDevice)Device;

            InsteonDeviceDictionary.Add(Dev.InsteonAddress, Dev);

            base.RegisterDevice(Device);
        }

        internal InsteonDevice GetDevice(InsteonAddress Address)
        {
            if (InsteonDeviceDictionary.ContainsKey(Address))
            {
                return InsteonDeviceDictionary[Address];
            }
            else
            {
                throw new Exception("No device found");
            }
        }

        #endregion 
    }
}
